﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;

namespace SimpleUnitity.Threading
{
    public class DelayCaller : IDisposable
    {
        static class Functions
        {
            static Functions()
            {
                ThreadPool.QueueUserWorkItem(new WaitCallback((obj) => CheckingThreadHandler()));
            }

            static readonly int checkingMilliseconds = 100;
            static object syncObject = new object();

            static object delayWaitObject = new object();

            static uint counter;

            static Dictionary<uint, DelayCaller> callers = new Dictionary<uint, DelayCaller>();

            static SortedList<uint, DelayCaller> checkingCallers = new SortedList<uint, DelayCaller>();

            public static void RemoteCaller(uint id)
            {
                lock (syncObject)
                {
                    if (checkingCallers.ContainsKey(id))
                    {
                        checkingCallers.Remove(id);
                        hasChangeCheckingSet = true;
                    }
                    if (callers.ContainsKey(id))
                    {
                        callers.Remove(id);
                    }
                }
                if (hasChangeCheckingSet)
                {

                    Monitor.TryEnter(delayWaitObject);
                    Monitor.Pulse(delayWaitObject);
                    Monitor.Exit(delayWaitObject);
                }
            }

            public static uint AddCaller(DelayCaller caller)
            {
                uint id = 0;
                lock (syncObject)
                {
                    id = ++counter;
                }
                callers.Add(id, caller);
                return id;
            }

            public static void DelayCaller(uint id)
            {
                lock (syncObject)
                {
                    if (checkingCallers.ContainsKey(id) == false)
                    {
                        checkingCallers.Add(id, callers[id]);
                        hasChangeCheckingSet = true;
                    }
                }
                Monitor.TryEnter(delayWaitObject);
                Monitor.Pulse(delayWaitObject);
                Monitor.Exit(delayWaitObject);
            }

            static bool hasChangeCheckingSet;
            static void CheckingThreadHandler()
            {
                DelayCaller[] items = new DelayCaller[0];
                bool isTimeout;
                bool removeChecking;
                while (true)
                {
                    lock (syncObject)
                    {
                        if (hasChangeCheckingSet)
                        {
                            items = new DelayCaller[checkingCallers.Values.Count];
                            int i = 0;
                            foreach (var item in checkingCallers)
                            {
                                items[i++] = item.Value;
                            }
                            hasChangeCheckingSet = false;
                        }
                    }
                    Thread.Sleep(checkingMilliseconds);
                    foreach (var item in items)
                    {
                        isTimeout = false;
                        removeChecking = false;
                        lock (syncObject)
                        {
                            if (item.totalSeconds > 0)
                            {
                                item.currentMilliseconds += checkingMilliseconds;
                                if (item.currentMilliseconds / 1000 >= item.totalSeconds)
                                {
                                    isTimeout = true;
                                    removeChecking = true;
                                }
                            }
                            else
                            {
                                removeChecking = true;
                            }
                        }
                        if (isTimeout)
                        {
                            item.CallTimeout();
                        }
                        if (removeChecking)
                        {
                            lock (syncObject)
                            {
                                hasChangeCheckingSet = true;
                                checkingCallers.Remove(item.id);
                            }
                        }
                    }
                    if (items.Length == 0)
                    {
                        Monitor.TryEnter(delayWaitObject);
                        Monitor.Wait(delayWaitObject);
                    }
                }

            }
        }    


        #region Events
        public event EventHandler OnTimeout;

        private void CallEvent(EventHandler handler)
        {
            if (handler != null)
            {
                handler(this, EventArgs.Empty);
            }
        }

        private void CallEvent<T>(EventHandler<T> handler, T eventArgs)
            where T : EventArgs
        {
            if (handler != null)
            {
                handler(this, eventArgs);
            }
        } 
        #endregion

        //private Stopwatch stopWatch = new Stopwatch();

        public DelayCaller()
        {
            id = Functions.AddCaller(this);
        }

        private int totalSeconds;
        private int currentMilliseconds;
        private readonly uint id;

        //readonly int checkingMilliseconds = 100;
        
        readonly object syncObject = new object();

        public void Delay(int seconds)
        {
            lock (syncObject) 
            {
                totalSeconds += seconds;
            }
            Functions.DelayCaller(id);
        }

        public void Reset()
        {
            lock (syncObject)
            {
                totalSeconds = 0;
                currentMilliseconds = 0;
            }
            Functions.DelayCaller(id);
        }

        private void CallTimeout()
        {
            ThreadPool.QueueUserWorkItem(new WaitCallback((obj) => CallEvent(OnTimeout)));
        }



        ~DelayCaller()
        {
            Functions.RemoteCaller(id);
        }

        public void Dispose()
        {
            Functions.RemoteCaller(id);
        }
    }
}
